package org.hackystat.iw.projectoverviewer.sensordata;

import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.PrintWriter;
import java.net.MalformedURLException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import javax.xml.datatype.XMLGregorianCalendar;
import org.hackystat.iw.projectoverviewer.jaxb.repositoryfiles.RepositoryFile;
import org.hackystat.iw.projectoverviewer.jaxb.repositoryfiles.RepositoryFiles;
import org.hackystat.iw.projectoverviewer.xml.XmlConverter;
import org.hackystat.sensorbase.client.SensorBaseClient;
import org.hackystat.sensorbase.client.SensorBaseClientException;
import org.hackystat.sensorbase.resource.sensordata.jaxb.Properties;
import org.hackystat.sensorbase.resource.sensordata.jaxb.Property;
import org.hackystat.sensorbase.resource.sensordata.jaxb.SensorData;
import org.hackystat.sensorbase.resource.sensordata.jaxb.SensorDataIndex;
import org.hackystat.sensorbase.resource.sensordata.jaxb.SensorDataRef;
import org.xml.sax.SAXException;
import com.meterware.httpunit.WebConversation;
import com.meterware.httpunit.WebLink;
import com.meterware.httpunit.WebResponse;

/**
 * A class to collect Hackystat sensor data.
 * 
 * @author Jianfei Liao
 */
public class SensorDataCollector {
  /** Default value for the "source lines" metric. */
  private static final String DEFAULT_SOURCE_LINES = "0";
  /** Default value for the "methods count" metric. */
  private static final String DEFAULT_METHODS_COUNT = "-1";
  /** Default value for the "coverage" metric. Negative value means not source file.*/
  private static final String DEFAULT_COVERAGE = "-1";
  /** Default value for the "last edited time" metric. */
  private static final String DEFAULT_LAST_EDITED_TIME = "04/01/2008";
  /** The sensor base client instance. */
  private SensorBaseClient client;
  /** The user email. */
  private String userEmail;
  /** A map storing the repository files, using the file names as the keys. */
  private Map<String, RepositoryFile> repositoryFilesMap = 
                                Collections.synchronizedMap(new HashMap<String, RepositoryFile>());
  /** A list storing the repository files. */
  private List<RepositoryFile> repositoryFilesList = 
                                Collections.synchronizedList(new ArrayList<RepositoryFile>());
  /** Determine whether the traverse of the repository is done. */
  private boolean doneTraverse;
  /** String token for " | ". **/
  private static final String BAR_TOKEN = " | ";
  
  /**
   * Collects sensor data and creates a list.
   * 
   * @param host location of repository
   * @param email login for hackystat account
   * @param password for hackystat account
   */
  public SensorDataCollector(String host, String email, String password) {
    // create a new sensorbase client
    this.userEmail = email;
    this.client = new SensorBaseClient(host, email, password);
    
    // set timeout to 10 minutes
    this.client.setTimeout(1000 * 60 * 10);

    // try to connect to the sensor base
    try {
      this.client.authenticate();
      System.out.println("INFO: Authentication succeeded.");
    }
    catch (SensorBaseClientException e) {
      System.out.println("*** AUTHENTICATION FAILED ***");
      e.printStackTrace();
    }
  }
  
  /**
   * Returns the sensor base client instance.
   * 
   * @return The sensor base client instance.
   */
  public synchronized SensorBaseClient getSensorBaseClient() {
    return this.client;
  }
  
  /**
   * Reads the project data file to get the previous sensor data, in case they are not presented
   * with the latest sensor data.
   * 
   * @param projectName The name of the project.
   */
  public synchronized void getOldSensorData(String projectName) {
    String userHome = System.getProperty("user.home");
    File xmlFile = new File(userHome + "/.hackystat/projectoverviewer/" + projectName + ".xml");
    String packageName = "org.hackystat.iw.projectoverviewer.jaxb.repositoryfiles";
    RepositoryFiles files = (RepositoryFiles) XmlConverter.xmlToObject(xmlFile, packageName);

    // get the old sensor data, for the files that is inside the repository only
    for (RepositoryFile file : files.getRepositoryFile()) {
      String fileName = file.getFileName();
      if (this.repositoryFilesMap.containsKey(fileName)) {
        RepositoryFile curFile = this.repositoryFilesMap.get(fileName);
        int fileIndex = this.repositoryFilesList.indexOf(curFile);
        curFile = this.repositoryFilesList.get(fileIndex);
    
        // update the current file using the old file sensor data
        curFile.setSourceLines(file.getSourceLines());
        curFile.setMethodsCount(file.getMethodsCount());
        curFile.setCoverage(file.getCoverage());
        curFile.setLastEditedTime(file.getLastEditedTime());
      }
    }
  }
  
  /**
   * Collects sensor data.
   * 
   * @param projectName The name of the project.
   * @param sensorType The type of sensor data to collect.
   * @param startTime The starting time.
   * @param endTime The ending time.
   */
  public synchronized void getSensorData(String projectName, String sensorType, 
      XMLGregorianCalendar startTime, XMLGregorianCalendar endTime) {
    
    // retrieve the specified type of sensor data
    try {
      System.out.println("INFO: Collecting sensor data of type: " + sensorType);
      SensorDataIndex sensorDataIndex = this.client.getProjectSensorData(this.userEmail, 
          projectName, startTime, endTime, sensorType);
      
      List<SensorDataRef> refList = sensorDataIndex.getSensorDataRef();

      // process each raw sensor data
      for (SensorDataRef ref : refList) {
        SensorData data = null;
        try {
          data = client.getSensorData(ref);  // raw sensor data
          Properties prop = data.getProperties(); // properties for different types of sensor data 
          List<Property> propList = prop.getProperty();
          
          // get the file path for each data
          String fileName = data.getResource();
          int index = fileName.lastIndexOf('/');
          fileName = fileName.substring(index + 1);
          index = fileName.lastIndexOf('\\');
          fileName = fileName.substring(index + 1);
          
          if ("FileMetric".equals(sensorType)) {
            processFileMetricData(fileName, propList, data);
          }
          
          else if ("DevEvent".equals(sensorType)) {
            processDevEventData(fileName, propList, data);
          }
          
          else if ("Coverage".equals(sensorType)) {
            processCoverageData(fileName, propList, data);
          }
        }
        catch (Exception e) {
          System.out.println("*** SENSOR DATA PROCESS FAILED. *** " + e.getMessage());
        }
      }
    }
    catch (Exception e) {
      System.out.println("*** SENSOR DATA REFERENCE RETRIVAL FAILED. *** " + e.getMessage());
    }
  }

  /**
   * Processes the coverage-type sensor data.
   * 
   * @param fileName The name of the file that the sensor data corresponds to. 
   * @param propList The list of type-specific properties of the data.
   * @param data The raw sensor data.
   */
  private void processCoverageData(String fileName, List<Property> propList, SensorData data) {
    System.out.print("Processing sensor data for " + fileName);
    
    String className = null;
    String coveredMethods = null;
    String uncoveredMethods = null;
    
    for (Property property : propList) {
      if ("ClassName".equals(property.getKey())) {
        className = property.getValue();
      }
      if ("method_Covered".equals(property.getKey())) {
        coveredMethods = property.getValue();
      }
      if ("method_Uncovered".equals(property.getKey())) {
        uncoveredMethods = property.getValue();
      }
    }
    
    if (this.repositoryFilesMap.containsKey(fileName)) {
      // get the class name
      /*className = propList.get(8).getValue();  // properties 8 is the className*/
      int classNameIndex = className.lastIndexOf('.');
      className = className.substring(classNameIndex + 1);

      // if the data corresponds to the public class of the file,
      // calculate the coverage using covered methods divided by total methods
      if (fileName.contains(className)) {
        // properties 0 is number of covered methods
        /*double covered = Double.parseDouble(propList.get(0).getValue());*/
        double covered = Double.parseDouble(coveredMethods);
        // properties 3 is number of uncovered methods
        /*double uncovered = Double.parseDouble(propList.get(3).getValue());*/
        double uncovered = Double.parseDouble(uncoveredMethods);
        double total = covered + uncovered;
        
        int coverage = (int) (covered / total * 100);
        int methodsCount = (int) total;
        
        System.out.println(BAR_TOKEN + covered + '/' + total + 
          '=' + coverage + '%');
      
        RepositoryFile file = this.repositoryFilesMap.get(fileName);
        int fileIndex = this.repositoryFilesList.indexOf(file);
        
        // update the list and map
        file = this.repositoryFilesList.get(fileIndex);
        file.setCoverage(coverage + "");
        file.setMethodsCount(methodsCount + "");
        this.repositoryFilesMap.put(fileName, file);
      }
      
      else {
        System.out.println(' ' + className + " is ignored because it's not a public class.");
      }
    }
    
    else {
      System.out.println(BAR_TOKEN + "File is not in repository yet or data is ignored.");
    }
  }

  /**
   * Process the DevEventData sensor data type.
   * 
   * @param fileName The name of the file that the sensor data corresponds to. 
   * @param propList The list of type-specific properties of the data.
   * @param data The raw sensor data.
   */
  private void processDevEventData(String fileName, List<Property> propList, SensorData data) {
    System.out.print("Processing sensor data for " + fileName);
    
    if (this.repositoryFilesMap.containsKey(fileName) &&
        !"Open".equals(propList.get(0).getValue()) &&  // properties 0 is the subtype
        !"Close".equals(propList.get(0).getValue())) {
      System.out.println(BAR_TOKEN + propList.get(0).getKey() + 
          BAR_TOKEN + propList.get(0).getValue() + BAR_TOKEN + data.getLastMod());
      
      // format the data to month/day/year
      String lastEditedTime = 
          String.format("%1$tm/%1$td/%1$tY", data.getLastMod().toGregorianCalendar());
      
      RepositoryFile file = this.repositoryFilesMap.get(fileName);
      int fileIndex = this.repositoryFilesList.indexOf(file);
      
      // update the list and map
      file = this.repositoryFilesList.get(fileIndex);
      file.setLastEditedTime(lastEditedTime);
      this.repositoryFilesMap.put(fileName, file);
    }
    
    else {
      System.out.println(BAR_TOKEN + "File is not in repository yet or data is ignored.");
    }
  }
  
  /**
   * Process the FileMetricData sensor data type.
   * 
   * @param fileName The name of the file that the sensor data corresponds to. 
   * @param propList The list of type-specific properties of the data.
   * @param data The raw sensor data.
   */
  private void processFileMetricData(String fileName, List<Property> propList, SensorData data) {
    System.out.print("Processing sensor data for " + fileName);
    String sourceLines = null;
    if (this.repositoryFilesMap.containsKey(fileName)) {
      for (Property property : propList) {
        if ("SourceLines".equals(property.getKey())) {
          sourceLines = property.getValue();
        }
      }
      System.out.println(BAR_TOKEN + "SourceLines" + 
          BAR_TOKEN + sourceLines); // properties 1 is number of source lines
      
      if (this.repositoryFilesMap.containsKey(fileName)) {
        RepositoryFile file = this.repositoryFilesMap.get(fileName);
        int fileIndex = this.repositoryFilesList.indexOf(file);
        
        // update the list and map
        file = this.repositoryFilesList.get(fileIndex);
        file.setSourceLines(sourceLines);
        this.repositoryFilesMap.put(fileName, file);
      }
    }
    
    else {
      System.out.println(BAR_TOKEN + "File is not in repository yet.");
    }
  }

  /**
   * Gets the repository information.
   * 
   * @param svnUrl The repository URL.
   * @param projectName The name of the project.
   * @return A map with the repository files.
   */
  public synchronized List<RepositoryFile> getRepository(String svnUrl, String projectName) {
    WebConversation wc = new WebConversation();
    try {
      WebResponse response = wc.getResponse(svnUrl);
      System.out.println("INFO: Start traversing the repository.");
      traverseRepository(response, null, projectName);
    }
    catch (MalformedURLException e) {
      e.printStackTrace();
    }
    catch (IOException e) {
      e.printStackTrace();
    }
    catch (SAXException e) {
      e.printStackTrace();
    }
    
    System.out.println("INFO: Done traversing.");
    return this.repositoryFilesList;
  }

  /**
   * Traverses thru the repository to collect files/directories information.
   * 
   * @param response The web response to the svn URL.
   * @param previousList The previous list.
   * @param projectName The name of the project.
   * @throws SAXException If errors occur.
   * @throws IOException If errors occur.
   */
  private synchronized void traverseRepository(WebResponse response, List<String> previousList,
      String projectName) throws SAXException, IOException {
    WebLink[] links = response.getLinks();
    
    while (!doneTraverse) {
      doneTraverse = true;
      
      for (WebLink link : links) {
        if (!link.getURLString().equalsIgnoreCase("../") &&
            !link.getURLString().equalsIgnoreCase(".classpath") &&
            !link.getURLString().equalsIgnoreCase(".project") &&
            !link.getURLString().equalsIgnoreCase("http://code.google.com/") &&
            !link.getURLString().equalsIgnoreCase("http://subversion.tigris.org/")) {
          if (link.getURLString().contains("/")) {
            doneTraverse = false;
            WebResponse nextResponse = link.click();
            traverseRepository(nextResponse, null, projectName);
          }
          
          else {
            String fileName = link.getURLString();
            String rootName = projectName;
            String filePath = response.getURL() + fileName;
            // truncate the unnecessary part of the file path string
            String path = "trunk/";
            int index = filePath.indexOf(path);
            filePath = rootName + '/' + filePath.substring(index + path.length());
             
            // create the repository file instance
            RepositoryFile file = new RepositoryFile();
            file.setFileName(fileName);
            file.setFilePath(filePath);

            // add the repository file to the map and the list
            this.repositoryFilesMap.put(fileName, file);
            this.repositoryFilesList.add(file);
          }
        }
      }
    }
  }
  
  /**
   * Writes a TM3 file base on the sensor data information.
   * 
   * @param tm3File The TM3 file to write.
   * @param xmlFile The XML file to write.
   * @throws FileNotFoundException If errors occur.
   */
  public synchronized void write(File tm3File, File xmlFile) throws FileNotFoundException {
    PrintWriter writer = null;
    RepositoryFiles files = new RepositoryFiles();
    String packageName = "org.hackystat.iw.projectoverviewer.jaxb.repositoryfiles";
    
    System.out.println("INFO: Start storing data to " + tm3File + ", " + xmlFile);
    try {
      writer = new PrintWriter(tm3File);
      writer.println("SourceLines\tMethodCounts\tCoverage\tLastEditedTime");
      writer.println("INTEGER\tINTEGER\tINTEGER\tDATE");
      
      for (RepositoryFile repositoryFile : this.repositoryFilesList) {
        String path = repositoryFile.getFilePath();
        // construct the file path, which uses a tab delimiter
        path = path.replace('/', '\t');
        
        // set values for different metrics
        String sourceLines = null;
        if (repositoryFile.getSourceLines() == null) {
          sourceLines = DEFAULT_SOURCE_LINES;
          repositoryFile.setSourceLines(sourceLines);
        }
        else {
          sourceLines = repositoryFile.getSourceLines();
        }
        
        String methodsCount = null;
        if (repositoryFile.getMethodsCount() == null) {
          methodsCount = DEFAULT_METHODS_COUNT;
          repositoryFile.setMethodsCount(methodsCount);
        }
        else {
          methodsCount = repositoryFile.getMethodsCount();
        }
        
        String coverage = null;
        if (repositoryFile.getCoverage() == null) {
          coverage = DEFAULT_COVERAGE;
          repositoryFile.setCoverage(coverage);
        }
        else {
          coverage = repositoryFile.getCoverage();
        }
        
        String lastEditedTime = null;
        if (repositoryFile.getLastEditedTime() == null) {
          lastEditedTime = DEFAULT_LAST_EDITED_TIME;
          repositoryFile.setLastEditedTime(lastEditedTime);
        }
        else {
          lastEditedTime = repositoryFile.getLastEditedTime();
        }
        
        // write the data to the TM3 file
        String info = sourceLines + '\t' + 
                      methodsCount + '\t' +
                      coverage + '\t' + 
                      lastEditedTime + "\t\t" + path;
        writer.println(info);
        
        // save the repository file instance to the repository files list instance
        files.getRepositoryFile().add(repositoryFile);
      }
      // convert the repository files instance to the xml file     
      XmlConverter.objectToXml(files, xmlFile, packageName);
    }
    finally {
      writer.close();
      System.out.println("INFO: Done storing data.");
    }
  }
}
